package com.lj.cloudDisk.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.j256.simplemagic.ContentInfoUtil;
import com.lj.cloudDisk.config.exception.SpaceException;
import com.lj.cloudDisk.mapper.FileMapper;
import com.lj.cloudDisk.mapper.UcenterMemberMapper;
import com.lj.cloudDisk.pojo.File;
import com.lj.cloudDisk.pojo.UcenterMember;
import com.lj.cloudDisk.service.FileService;
import com.lj.cloudDisk.service.MediaFileService;
import com.j256.simplemagic.ContentInfo;
import com.lj.cloudDisk.service.UcenterMemberService;
import io.minio.*;
import io.minio.errors.*;
import io.minio.messages.DeleteError;
import io.minio.messages.DeleteObject;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.compress.utils.IOUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;
import org.apache.commons.codec.digest.DigestUtils;

import java.io.*;
import java.nio.file.*;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;


@Slf4j
@Service
public class MediaFileServiceImpl implements MediaFileService {

    @Autowired
    MinioClient minioClient;

    @Autowired
    MediaFileService currentProxy;

    @Autowired
    UcenterMemberMapper ucenterMemberMapper;

    @Autowired
    UcenterMemberService ucenterMemberService;

    @Autowired
    FileMapper fileMapper;

    //存储普通文件
    @Value("${minio.bucket.files}")
    private String bucket_mediaFiles;

    //存储视频
    @Value("${minio.bucket.videoFiles}")
    private String bucket_video;

    //存储本地分块路径
    @Value("${minio.bucket.chunkPath}")
    private String chunkPath;

    //分块大小
    @Value("${minio.bucket.chunkSize}")
    private String chunkSize;

    @Autowired
    private FileService fileService;

    /**
     * @description:判断文件是否存在
     * @author L J
     * @date 2023/8/28 9:44
     * @param memId String
     * @param catalogue String
     * @return boolean
     */
    @Override
    public boolean fileIsExist(String memId, String catalogue,String fileName) {

        //查询当前目录下的文件
        List<File> curDirFiles = fileService.getCurDirFiles(memId, catalogue);
        List<String> curDirFilesName = curDirFiles.stream().map(item -> {
            return item.getName()+"." +item.getType();
        }).collect(Collectors.toList());
        log.debug(fileName);
        log.debug(curDirFilesName.toString());
        curDirFilesName.add(fileName);
        log.debug(curDirFilesName.toString());
         Set set = new HashSet<>(curDirFilesName);
         if(set.size() != curDirFilesName.size()){
             return false;
         }

       return true;
    }

    /**
     * @description: 删除文件 以及minio中的文件
     * @author L J
     * @date 2023/8/29 8:47
     * @param idList String
     * @param memId String
     * @return boolean
     */
    @Override
    public boolean removeFile(String[] idList, String memId) {
        UcenterMember userInfo = ucenterMemberService.getUserInfo(memId);
        long result = userInfo.getNeicun();
        for (int i = 0; i < idList.length; i++) {
            LambdaQueryWrapper<File> queryWrapper = new LambdaQueryWrapper<>();
            queryWrapper.eq(File::getId,memId);
            File file = fileService.getById(idList[i]);
            result -= - file.getSize();
            //删除音频
            if(file.getFiletype().equals("video")|| file.getFiletype().equals("audio")){

                    //将minio中文件删除
                boolean b = deleteToMinIo(bucket_video, file.getUrl());
                if(!b){
                    throw new SpaceException(20001,"minio文件删除失败");
                }

                //将数据库中的文件删除
                    fileMapper.deleteById(idList[i]);
            }else{
                //删除图片以及其他文件
                boolean b = deleteToMinIo(bucket_mediaFiles,file.getUrl());
                if(!b){
                    throw new SpaceException(20001,"minio文件删除失败");
                }
                //将数据库中的文件删除
                fileMapper.deleteById(idList[i]);
            }

        }
        return false;
    }


    /**
      * @description:上传文件
      * @author L J
      * @date 2023/8/21 20:38
      * @param file File 文件详细
      * @param memId String 用户id
      * @param localFilePath String 原始文件路径
      * @return File
      */
    @Override
    public File uploadFile(File file, String memId, String localFilePath) {
            //获取用户信息
            LambdaQueryWrapper <UcenterMember>queryWrapper = new LambdaQueryWrapper<>();
            queryWrapper.eq(UcenterMember::getId,memId);
            UcenterMember member = ucenterMemberMapper.selectOne(queryWrapper);
            long neicun = member.getNeicun();
            long size = file.getSize();
            long result= neicun + size;
            if(result < 1073741824) {
                UcenterMember newMember = new UcenterMember();
                newMember.setNeicun(result);
                newMember.setId(memId);
                //更新用户信息  剩余内存
                ucenterMemberService.updateById(newMember);

                //文件名
                String filename = file.getName();
                //先得到扩展名
                String extension = filename.substring(filename.lastIndexOf("."));
                //得到mimeType
                String mimeType = getMimeType(extension);
                //子目录
                String defaultFolderPath = getDefaultFolderPath();
                //文件的md5值
                String fileMd5 = getFileMd5(new java.io.File(localFilePath));
                //设置目录加文件的名字
                String objectName = defaultFolderPath + fileMd5 + extension;

                String type = extension.substring(1);

                //获取文件名称
                String title = filename.substring(0, filename.lastIndexOf("."));
                //判断文件类型
                if (type.equals("mp3") || type.equals("mpeg") || type.equals("vma") || type.equals("aac") || type.equals("ra") || type.equals("am") || type.equals("rmx") || type.equals("mp3")
                        || type.equals("avi") || type.equals("mov") || type.equals("rmvb") || type.equals("rm") || type.equals("mp4") || type.equals("3gp") || type.equals("flv") || type.equals("ape") || type.equals("flac") || type.equals("wmv")) {

                    File newFile = new File();
                    newFile.setSize(size);
                    if (type.equals("mp3") || type.equals("mpeg") || type.equals("vma") || type.equals("aac") || type.equals("ra") || type.equals("am") || type.equals("rmx")
                            || type.equals("ape") || type.equals("flac")) {
                        newFile.setFiletype("audio");
                    }
                    if (type.equals("avi") || type.equals("mov") || type.equals("wmv") || type.equals("rmvb") || type.equals("rm") || type.equals("mp4") || type.equals("3gp") || type.equals("flv")) {
                        newFile.setFiletype("video");
                    }
                    newFile.setName(title);
                    newFile.setType(type);

                    int chunkTotal;
                    //开始分块 并检查分块
                    try {
                        chunkTotal = chunkAndCheck(localFilePath, fileMd5);
                    } catch (Exception e) {
                        throw  new SpaceException(20001,"文件分块出现异常");
                    }
                    //合并分块
                    Map<String, Object> map = mergeChunks(fileMd5, chunkTotal, file);

                    //合并后的文件地址
                    String url = (String)map.get("url");
                    //文件大小
                     size = (long)map.get("size");
                     newFile.setUrl("/" + bucket_video + "/" + url);
                     newFile.setSize(size);
                     newFile.setVideoId(fileMd5);
                    return  newFile;
                } else{
                    //上传文件到minio
                    boolean res = addMediaFilesToMinIO(localFilePath, mimeType,bucket_mediaFiles, objectName);
                    if(res){
                        File newFile = new File();
                        if(type.equals("bmp")||type.equals("mp3")||type.equals("jpg")||type.equals("jpeg")||type.equals("png")||type.equals("mp3")||type.equals("tif")||type.equals("gif")
                                ||type.equals("pcx")||type.equals("tga")||type.equals("exif")||type.equals("fpx")||type.equals("svg")||type.equals("psd")||type.equals("cdr")
                                ||type.equals("pcd")||type.equals("dxf")||type.equals("ufo")||type.equals("eps")||type.equals("ai")||type.equals("raw")
                                ||type.equals("WMF")||type.equals("webp")||type.equals("mp3")||type.equals("avif")){
                            newFile.setFiletype("image");
                        }
                        newFile.setFDir(file.getFDir());
                        newFile.setSize(size);
                        newFile.setName(title);
                        newFile.setUrl("/" + bucket_mediaFiles + "/" +objectName);
                        newFile.setType(type);
                        return newFile;
                    }
                    throw new SpaceException(20001,"上传文件失败");
                }

            }else {
                throw new SpaceException(20001,"内存溢出");
            }


    }




    /**
     * @description: 进行分块 检查分块 删除分块
     * @author L J
     * @date 2023/8/22 21:38
     * @param localFilePath String
     * @return int 文件块数
     */
    public int chunkAndCheck(String localFilePath,String fileMd5){

        //源文件
        java.io.File sourceFile = new java.io.File(localFilePath);

        //分块文件个数
        int chuckNum = (int)Math.ceil(sourceFile.length() * 1.0 / Integer.parseInt(chunkSize));

        try {
            //缓冲区
            byte bytes[] = new byte[1024];
            //使用流对源文件进行读数据，向分块中读取数据
            RandomAccessFile raf_r = new RandomAccessFile(sourceFile, "r");
            java.io.File file = new java.io.File(chunkPath);
            //判断分块目录是否存在
            if(!file.exists()){
                file.mkdirs();
            }
            for (int i = 0; i < chuckNum; i++) {
                java.io.File chunkFile = new java.io.File(chunkPath + i);
                //对文件写入流
                RandomAccessFile raf_rw = new RandomAccessFile(chunkFile, "rw");
                int len = -1;
                while((len = raf_r.read(bytes)) != -1){
                        raf_rw.write(bytes,0,len);
                        if(chunkFile.length() >= Integer.parseInt(chunkSize)){
                                    break;
                        }
                }
                raf_rw.close();
                //将分块文件上传minio
                uploadChunk(fileMd5,i,chunkPath + i);
                //检查分块
                checkChunk(fileMd5,i);

            }
            raf_r.close();

            //删除出本地分块文件和分块目录
            deleteDirectory(Paths.get(chunkPath));


        } catch (IOException e) {
            throw new SpaceException(20001,"文件上传失败");
        }
            return chuckNum;
    }


    /**
     * @param fileMd5             文件md5
     * @param chunkTotal          分块总和
     * @param file 文件信息
     * @return com.xuecheng.base.model.RestResponse
     * @description 合并分块
     */
    public Map<String,Object> mergeChunks( String fileMd5, int chunkTotal, File file) {

        //用于保存文件信息
        Map<String, Object> map = new HashMap<>();

        //找到分块文件调用minio的sdk进行文件合并
        //stream
        //分块文件所建目录
        String chunkFileFolderPath = getChunkFileFolderPath(fileMd5);
        List<ComposeSource> composeSources = Stream.iterate(0, i -> ++i).limit(chunkTotal).map(i -> ComposeSource.builder().bucket(bucket_video).object(chunkFileFolderPath + i).build()).collect(Collectors.toList());

        //源文件名
        String filename = file.getName();

        //扩展名
        String extension = filename.substring(filename.lastIndexOf("."));

        //合并后的文件名
        String filePathByMd5 = getFilePathByMd5(fileMd5, extension);
        map.put("url",filePathByMd5);

        //合并后的objectName等信息
        ComposeObjectArgs composeObjectArgs = ComposeObjectArgs
                .builder()
                .bucket(bucket_video)
                .object(filePathByMd5)//最终合并的文件名
                .sources(composeSources)//指定源文件
                .build();

        //合并文件
        //报错
        try {
            minioClient.composeObject(composeObjectArgs);
        } catch (Exception e) {

            e.printStackTrace();
            log.error("合并文件出错,bucket{},objectName{},错误信息{}", bucket_video, filePathByMd5, e.getMessage());
            throw  new SpaceException(20001,"合并文件出错");

        }
        //校验合并后的和源文件是否一至，视频上传才成功
        //先下载合并后的文件
        java.io.File fileMinio = downloadFileFromMinIO(bucket_video, filePathByMd5);
        //计算合并后的文件
        try (FileInputStream fileInputStream = new FileInputStream(fileMinio)) {
            //计算合并后文件的md5值
            String mergeFile_md5 = DigestUtils.md5Hex(fileInputStream);
            //比较原始的md5和合并后文件的md5
            if (!fileMd5.equals(mergeFile_md5)) {
                log.error("校验合并文件的md5不一致，原始文件{}，合并文件{}", mergeFile_md5, fileMd5);
                throw new SpaceException(20001,"校验文件失败");
            }
            //文件大小
           map.put("size",fileMinio.length());
        } catch (Exception e) {
            throw new SpaceException(20001,"校验文件失败");
        }

        //清理分块文件
        clearChunkFiles(chunkFileFolderPath, chunkTotal);


        return map;
    }





    //删除文件以及文件夹
    public static void deleteDirectory(Path path) throws IOException {
        if (Files.isDirectory(path, LinkOption.NOFOLLOW_LINKS)) {
            try (DirectoryStream<Path> entries = Files.newDirectoryStream(path)) {
                for (Path entry : entries) {
                    deleteDirectory(entry);
                }
            }
        }

        Files.delete(path);
    }






    /**
     * 将文件上传到minio
     *
     * @param localFilePath 文件本地路径
     * @param mimeType      媒体类型
     * @param bucket        桶
     * @param objectName    对象名
     * @return
     */
    public boolean addMediaFilesToMinIO(String localFilePath, String mimeType, String bucket, String objectName) {
        try {
            UploadObjectArgs uploadObjectArgs = UploadObjectArgs.builder()
                    .bucket(bucket)//桶
                    .filename(localFilePath) //指定本地文件路径
                    .object(objectName)//对象名 放在子目录下
                    .contentType(mimeType)//设置媒体文件类型
                    .build();
            //上传文件
            minioClient.uploadObject(uploadObjectArgs);
            log.debug("上传文件到minio成功,bucket:{},objectName:{},错误信息:{}", bucket, objectName);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            log.error("上传文件出错,bucket:{},objectName:{},错误信息:{}", bucket, objectName, e.getMessage());
        }
        return false;
    }



    /**
     * @description:检查分块
     * @author L J
     * @date 2023/8/22 21:34
     * @param fileMd5 String
     * @param chunkIndex int
     * @return Boolean
     */
    public Boolean checkChunk(String fileMd5, int chunkIndex) {
        //分块存储路径是: md5前两位为两个目录，
        //chunk存储分块文件根据md5得到分块文件的路径
        String chunkFileFolderPath = getChunkFileFolderPath(fileMd5);

        String chunkFilePath = chunkFileFolderPath + chunkIndex;

        //如果数据库存在再查询minio
        GetObjectArgs getObjectArgs = GetObjectArgs.builder()
                .bucket(bucket_video)
                .object(chunkFilePath)
                .build();
        //查询远程服务获取到一个流对象
        try {
            FilterInputStream inputStream = minioClient.getObject(getObjectArgs);
            if (inputStream != null) {
                return true;
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

        return false;

    }



    /**
     * @description:上传分块文件
     * @author L J
     * @date 2023/8/22 21:28
     * @param fileMd5 String
     * @param chunk int
     * @param localChunkFilePath String
     * @return void
     */
    public boolean uploadChunk(String fileMd5, int chunk, String localChunkFilePath) {
        String mimeType = getMimeType(null);
        //分块文件路径
        String chunkFilePath = getChunkFileFolderPath(fileMd5) + chunk;
        //将分块文件上传到minion中
        boolean b = addMediaFilesToMinIO(localChunkFilePath, mimeType, bucket_video, chunkFilePath);

       return b;
    }


    /**
     * @description:删除mminio中的文件
     * @author L J
     * @date 2023/8/29 9:28
     * @param bucket String 桶
     * @param filePath 文件路径
     * @return boolean
     */
    public boolean deleteToMinIo(String bucket,String filePath){


        String objectName = getObjectName(filePath);
        try {
            RemoveObjectArgs build = RemoveObjectArgs.builder().bucket(bucket).object(objectName).build();
            minioClient.removeObject(build);
        } catch (Exception e) {
            return false;
        }
        return true;
    }


    /**
     * 从minio下载文件
     * @param bucket     桶
     * @param objectName 对象名称
     * @return 下载后的文件
     */
    public java.io.File downloadFileFromMinIO(String bucket, String objectName) {
        //临时文件
        java.io.File minioFile = null;
        FileOutputStream outputStream = null;
        try {
            InputStream stream = minioClient.getObject(GetObjectArgs.builder()
                    .bucket(bucket)
                    .object(objectName)
                    .build());
            //创建临时文件
            minioFile = java.io.File.createTempFile("minio", ".merge");
            outputStream = new FileOutputStream(minioFile);
            IOUtils.copy(stream, outputStream);

            return minioFile;
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (outputStream != null) {
                try {
                    outputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        return null;
    }


    /**
     * 清除分块文件
     *
     * @param chunkFileFolderPath 分块文件路径
     * @param chunkTotal          分块文件总数
     */
    private void clearChunkFiles(String chunkFileFolderPath, int chunkTotal) {

        try {
            List<DeleteObject> deleteObjects = Stream.iterate(0, i -> ++i)
                    .limit(chunkTotal)
                    .map(i -> new DeleteObject(chunkFileFolderPath.concat(Integer.toString(i))))
                    .collect(Collectors.toList());

            RemoveObjectsArgs removeObjectsArgs = RemoveObjectsArgs.builder().bucket(bucket_video).objects(deleteObjects).build();
            Iterable<Result<DeleteError>> results = minioClient.removeObjects(removeObjectsArgs);
            results.forEach(r -> {
                DeleteError deleteError = null;
                try {
                    deleteError = r.get();
                } catch (Exception e) {
                    e.printStackTrace();
                    log.error("清楚分块文件失败,objectname:{}", deleteError.objectName(), e);
                }
            });
        } catch (Exception e) {
            e.printStackTrace();
            log.error("清楚分块文件失败,chunkFileFolderPath:{}", chunkFileFolderPath, e);

        }
    }



    //得到分块文件的目录
    private String getChunkFileFolderPath(String fileMd5) {
        return fileMd5.substring(0, 1) + "/" + fileMd5.substring(1, 2) + "/" + fileMd5 + "/" + "chunk" + "/";
    }



    //根据扩展名获取mimeType
    private String getMimeType(String extension) {
        if (extension == null) {
            extension = "";
        }
        //根据扩展名取出mimeType
        ContentInfo extensionMatch = ContentInfoUtil.findExtensionMatch(extension);
        String mimeType = MediaType.APPLICATION_OCTET_STREAM_VALUE;//通用mimeType，字节流
        if (extensionMatch != null) {
            mimeType = extensionMatch.getMimeType();
        }
        return mimeType;

    }



    //获取文件默认存储目录路径 年/月/日
    private String getDefaultFolderPath() {
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd-HHmmss");
        String folder = sdf.format(new Date()).replace("-", "/") + "/";
        return folder;
    }

    //获取文件的md5
    private String getFileMd5(java.io.File file) {
        try (FileInputStream fileInputStream = new FileInputStream(file)) {
            String fileMd5 = DigestUtils.md5Hex(fileInputStream);
            return fileMd5;
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

    /**
     * 得到合并后的文件的地址
     *
     * @param fileMd5 文件id即md5值
     * @param fileExt 文件扩展名
     * @return
     */
    private String getFilePathByMd5(String fileMd5, String fileExt) {
        return fileMd5.substring(0, 1) + "/" + fileMd5.substring(1, 2) + "/" + fileMd5 + "/" + fileMd5 + fileExt;
    }

    /**
     * @description:去除桶
     * @author L J
     * @date 2023/8/29 10:55
     * @param
     * @return String
     */
    private static String getObjectName(String filePath){
        String objectName = filePath.substring(filePath.indexOf("/",1)+1);
        return  objectName;
    }
}
